<!-- uni-app内使用RecordApp录音
GitHub: https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp
DCloud 插件市场下载组件: https://ext.dcloud.net.cn/plugin?name=Recorder-UniCore
-->

<template>
  <view>
    <view style="padding: 5px 10px 0">
      <text style="vertical-align: top"></text>

      <navigator :url="pageNewPath" style="display: inline">
        <button size="mini" type="default">新开页面({{ pageDeep }})</button>
      </navigator>

      <navigator url="page_i18n" style="display: inline">
        <button size="mini" type="default">国际化多语言</button>
      </navigator>

      <navigator url="page_asr" style="display: inline">
        <button size="mini" type="default">asr语音识别</button>
      </navigator>

      <!-- #ifdef APP || H5 -->
      <navigator url="page_nvue" style="display: inline">
        <button size="mini" type="default">nvue原生页面</button>
      </navigator>
      <navigator url="page_renderjsOnly" style="display: inline">
        <button size="mini" type="default">纯renderjs调用</button>
      </navigator>
      <!-- #endif -->

      <button size="mini" type="default" @click="reloadPage">刷新当前页</button>
    </view>
    <view style="margin-top: 5px; height: 10px; background: #eee"></view>

    <!-- 录音格式选择 -->
    <view style="padding: 10px 10px 0">
      类型：
      <checkbox
        @click="recTypeClick"
        :checked="recType == 'mp3'"
        data-type="mp3"
        >mp3</checkbox
      >
      <checkbox
        @click="recTypeClick"
        :checked="recType == 'wav'"
        data-type="wav"
        >wav</checkbox
      >
      <checkbox
        @click="recTypeClick"
        :checked="recType == 'pcm'"
        data-type="pcm"
        >pcm</checkbox
      >
      <checkbox
        @click="recTypeClick"
        :checked="recType == 'amr'"
        data-type="amr"
        >amr</checkbox
      >
      <checkbox
        @click="recTypeClick"
        :checked="recType == 'g711a'"
        data-type="g711a"
        >g711a</checkbox
      >
      <checkbox
        @click="recTypeClick"
        :checked="recType == 'g711u'"
        data-type="g711u"
        >g711u</checkbox
      >
      <checkbox
        @click="recTypeClick"
        :checked="recType == 'ogg'"
        data-type="ogg"
        :disabled="disableOgg"
        >ogg{{ disableOgg ? "(js太大)" : "" }}</checkbox
      >
    </view>
    <view style="padding: 10px 10px 0">
      采样率：<input
        type="number"
        v-model.number="recSampleRate"
        style="width: 60px; display: inline-block; border: 1px solid #ddd"
      />hz 比特率：<input
        type="number"
        v-model.number="recBitRate"
        style="width: 60px; display: inline-block; border: 1px solid #ddd"
      />kbps
    </view>

    <!-- 控制按钮 -->
    <view style="display: flex; padding-top: 10px">
      <view style="width: 10px"></view>
      <view style="flex: 1">
        <button type="warn" @click="recReq" style="font-size: 16px; padding: 0">
          请求录音权限
        </button>
      </view>
      <view style="width: 10px"></view>
      <view style="flex: 1">
        <button
          type="primary"
          @click="recStart"
          style="font-size: 16px; padding: 0"
        >
          开始录音
        </button>
      </view>
      <view style="width: 10px"></view>
      <view style="flex: 1">
        <button @click="recStop" style="font-size: 16px; padding: 0">
          停止录音
        </button>
      </view>
      <view style="width: 10px"></view>
    </view>
    <view style="padding: 10px 10px 0">
      <button size="mini" type="default" @click="recPause">暂停</button>
      <button size="mini" type="default" @click="recResume">继续</button>
      <button
        size="mini"
        type="default"
        @click="recStopX"
        style="padding: 0 5px; margin-left: 10px"
      >
        停止(仅清理)
      </button>
      <button
        size="mini"
        type="default"
        @click="recEnvIn60"
        style="padding: 0 5px"
      >
        注入60秒数据
      </button>
    </view>

    <!-- 可视化绘制 -->
    <view style="padding: 5px 0 0 10px">
      <view
        style="height: 40px; width: 300px; background: #999; position: relative"
      >
        <view
          style="height: 40px; background: #0b1; position: absolute"
          :style="{ width: recpowerx + '%' }"
        ></view>
        <view
          style="padding-left: 50px; line-height: 40px; position: relative"
          >{{ recpowert }}</view
        >
      </view>

      <!-- 不同的波形样式，只需创建需要用到的canvas就行，canvas需要指定宽高（下面style里指定了300*100） -->
      <view style="padding-top: 5px"></view>
      <view>
        <view
          class="recwave"
          :style="{ display: recwaveChoiceKey != 'WaveView' ? 'none' : '' }"
        >
          <canvas type="2d" class="recwave-WaveView"></canvas>
        </view>
        <view
          class="recwave"
          :style="{ display: recwaveChoiceKey != 'SurferView' ? 'none' : '' }"
        >
          <canvas type="2d" class="recwave-SurferView"></canvas>
          <canvas
            type="2d"
            class="recwave-SurferView-2x"
            style="width: 600px; display: none"
          ></canvas>
        </view>
        <view
          class="recwave"
          :style="{ display: recwaveChoiceKey != 'Histogram1' ? 'none' : '' }"
        >
          <canvas type="2d" class="recwave-Histogram1"></canvas>
        </view>
        <view
          class="recwave"
          :style="{ display: recwaveChoiceKey != 'Histogram2' ? 'none' : '' }"
        >
          <canvas type="2d" class="recwave-Histogram2"></canvas>
        </view>
        <view
          class="recwave"
          :style="{ display: recwaveChoiceKey != 'Histogram3' ? 'none' : '' }"
        >
          <canvas type="2d" class="recwave-Histogram3"></canvas>
        </view>
      </view>

      <view style="padding-top: 5px" @click="recwaveChoice">
        <view
          class="recwaveChoice"
          :class="recwaveChoiceKey == 'WaveView' ? 'slc' : ''"
          data-key="WaveView"
          >WaveView</view
        >
        <view
          class="recwaveChoice"
          :class="recwaveChoiceKey == 'SurferView' ? 'slc' : ''"
          data-key="SurferView"
          >SurferView</view
        >
        <view
          class="recwaveChoice"
          :class="recwaveChoiceKey == 'Histogram1' ? 'slc' : ''"
          data-key="Histogram1"
          >Histogram1</view
        >
        <view
          class="recwaveChoice"
          :class="recwaveChoiceKey == 'Histogram2' ? 'slc' : ''"
          data-key="Histogram2"
          >H...2</view
        >
        <view
          class="recwaveChoice"
          :class="recwaveChoiceKey == 'Histogram3' ? 'slc' : ''"
          data-key="Histogram3"
          >H...3</view
        >
      </view>
    </view>

    <view style="padding: 5px 10px 0">
      <view
        ><checkbox
          :checked="takeoffEncodeChunkSet"
          @click="takeoffEncodeChunkSet = !takeoffEncodeChunkSet"
          >接管编码器输出（takeoffEncodeChunk，App端推荐开启）同时清理内存来长时间录音</checkbox
        >
        {{ takeoffEncodeChunkMsg }}</view
      >
      <view
        ><checkbox :checked="appUseH5Rec" @click="appUseH5RecClick"
          >App里面总是使用Recorder
          H5录音（勾选后不启用原生录音插件和uts插件）</checkbox
        ></view
      >

      <view
        ><checkbox :checked="useAEC" @click="useAEC = !useAEC"
          >尝试启用回声消除（echoCancellation）</checkbox
        ></view
      >

      <view
        ><checkbox :checked="showUpload" @click="showUpload = !showUpload"
          >录音上传、保存本地文件+播放、实时语音通话聊天对讲（WebSocket）</checkbox
        ></view
      >
    </view>

    <!-- 手撸播放器 -->
    <view style="padding-top: 10px">
      <TestPlayer ref="player" />
    </view>

    <!-- 上传、语音通话等功能 -->
    <view v-show="showUpload">
      <TestUploadView />
      <TestRtVoiceView />
    </view>

    <!-- 日志输出 -->
    <view style="padding-top: 10px">
      <view
        v-for="obj in reclogs"
        :key="obj.idx"
        style="border-bottom: 1px dashed #666; padding: 5px 0"
      >
        <view
          :style="{
            color:
              obj.color == 1 ? 'red' : obj.color == 2 ? 'green' : obj.color,
          }"
        >
          {{ obj.txt }}
        </view>
      </view>
      <view
        v-if="reclogLast"
        style="
          position: fixed;
          z-index: 2;
          width: 20vw;
          min-width: 200px;
          max-height: 100px;
          overflow: auto;
          right: 0;
          bottom: 0;
          background: #fff;
          padding: 5px 10px;
          border-radius: 6px 0 0 0;
          box-shadow: -1px -1px 3px #ddd;
          font-size: 13px;
        "
      >
        <view
          :style="{
            color:
              reclogLast.color == 1
                ? 'red'
                : reclogLast.color == 2
                ? 'green'
                : reclogLast.color,
          }"
        >
          {{ reclogLast.txt }}
        </view>
      </view>
    </view>
    <view style="padding-top: 80px"></view>

    <!-- 更多功能按钮 -->
    <view style="padding: 10px 10px">
      <view>
        <navigator url="page_vue3____composition_api" style="display: inline">
          <button size="mini" type="default">vue3组合式api页面</button>
        </navigator>

        <button size="mini" type="default" @click="loadVConsole">
          显示vConsole
        </button>
      </view>
      <!-- #ifdef APP -->
      <view>
        <navigator url="page_renderjsWithout" style="display: inline">
          <button size="mini" type="default">
            逻辑层编码UniWithoutAppRenderjs后台录音更稳
          </button>
        </navigator>
      </view>
      <view>
        <button size="mini" type="default" @click="testRenderjsFunc">
          测试renderjs功能调用
        </button>
        <TestPageRenderjsView ref="testRF" />
      </view>
      <view style="padding-top: 10px">
        <button size="mini" type="default" @click="testShowNotifyService">
          显示后台录音保活通知(Android)
        </button>
        <button size="mini" type="default" @click="testCloseNotifyService">
          关闭通知
        </button>
      </view>
      <view style="padding-bottom: 10px; font-size: 14px">
        <checkbox
          :checked="useANotifySrv"
          @click="useANotifySrv = !useANotifySrv"
          >本页面录音时自动尝试打开保活(Android)</checkbox
        >
      </view>
      <!-- #endif -->
      <view>
        <button size="mini" type="default" @click="speakerOnClick">
          切换成扬声器外放
        </button>
        <button size="mini" type="default" @click="speakerOffClick">
          切换成听筒播放
        </button>
      </view>
      <!-- #ifdef APP -->
      <view>
        <button size="mini" type="default" @click="testNativePlugin">
          测试原生插件调用
        </button>
        <button size="mini" type="default" @click="testShowMemoryUsage">
          显示内存占用
        </button>
        <TestNativePluginView ref="testNP" />
      </view>
      <!-- #endif -->

      <view>
        <button size="mini" type="default" @click="clearLogs">清除日志</button>
        <!-- #ifdef APP || H5 -->
        <button size="mini" type="default" @click="testH5Play5F">
          播放5分钟wav
        </button>
        <!-- #endif -->
      </view>

      <view class="testH5Play5FView"></view>
      <view>
        <view
          v-for="item in testMsgs"
          style="border-top: 1px dashed #eee; padding: 5px 0"
          :style="{
            color:
              item.color == 1 ? 'red' : item.color == 2 ? 'green' : item.color,
          }"
        >
          {{ item.msg }}
        </view>
      </view>
    </view>

    <!-- 文档地址提示 -->
    <view style="height: 10px; background: #eee"></view>
    <view style="padding: 10px 10px">
      <view style="padding-bottom: 10px; color: #02a2ff; word-break: break-all">
        <view style="padding-bottom: 10px"
          >DCloud 插件市场下载本组件:
          https://ext.dcloud.net.cn/plugin?name=Recorder-UniCore</view
        >
        <view
          >RecordApp的uni-app支持文档和示例:
          https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_UniApp
          (github可以换成gitee)</view
        >
      </view>
      <view style="color: #0ab; font-size: 22px; font-weight: bold">
        如需录音功能定制开发，网站、App、小程序、前端后端开发等需求，请加QQ群：①群
        781036591、②群 748359095、③群
        450721519，口令recorder，联系群主（即作者），谢谢~
      </view>
    </view>

    <!-- 组件说明 -->
    <view style="margin-bottom: 5px; height: 10px; background: #eee"></view>
    <view style="padding: 0 10px">
      <view style="font-size: 17px; font-weight: bold; color: #f60"
        >关于Recorder-UniCore组件中的 app-uni-support.js 支持文件</view
      >

      <view style="font-size: 14px; padding-top: 10px">
        <view
          >Recorder-UniCore组件中给RecordApp提供uni-app适配的代码在app-uni-support.js文件内，此文件为压缩版（功能和源码版一致），如果已获得商用授权、或者付费购买了配套的原生录音插件或uts插件后，可在VIP支持QQ群的群文件中下载到此js文件最新源码。</view
        >
        <view style="color: #f60"
          >app-uni-support.js文件在uni-app中编译到App平台时仅供测试用（App平台包括：Android
          App、iOS App），<text style="font-weight: bold"
            >不可用于正式发布或商用，正式发布或商用需先联系作者获得授权许可</text
          >（如何获取授权请阅读Recorder-UniCore组件文档）。</view
        >
        <view style="color: #0b0"
          >编译到其他平台时无此授权限制，比如：H5、小程序，均为免费授权。</view
        >
      </view>
    </view>

    <!-- #ifdef APP || H5 -->
    <!-- Eval执行代码，方便调试 -->
    <view style="padding-top: 80px"></view>
    <view style="height: 10px; background: #eee"></view>
    <view style="padding: 10px 10px">
      <view>
        <text style="font-size: 17px; font-weight: bold; color: #f60"
          >在逻辑层中执行代码</text
        >
        <button size="mini" type="default" @click="evalExecClick">执行</button>
      </view>
      <textarea
        v-model="evalExecCode"
        maxlength="-1"
        style="
          display: block;
          border: 1px solid #ddd;
          box-sizing: border-box;
          width: 100%;
        "
        placeholder="this为当前页面或组件的this"
      ></textarea>
    </view>
    <!-- #endif -->
  </view>
</template>

<script>
/**本例子是VUE的选项式 API (Options API)风格，vue2 vue3均支持，如果你使用的vue3 setup组合式 API (Composition API) 编写时，请在import后面取到当前实例this，在需要this的地方传vue3This变量即可，具体调用可以参考 page_vue3____composition_api.vue
import { getCurrentInstance } from 'vue';
var vue3This=getCurrentInstance().proxy; //必须定义到最外面，放import后面即可 */

import TestPlayer from './test_player___.vue'; //手撸的一个跨平台播放器
import TestUploadView from './test_upload_saveFile.vue'; //上传功能界面
import TestRtVoiceView from './test_realtime_voice.vue'; //实时语音通话聊天对讲
import TestNativePluginView from './test_native_plugin.vue'; //测试原生插件功能
import TestPageRenderjsView from './test_page_renderjs.vue'; //测试renderjs功能调用


/** 先引入Recorder （ 需先 npm install recorder-core ）**/
import Recorder from 'recorder-core'; //注意如果未引用Recorder变量，可能编译时会被优化删除（如vue3 tree-shaking），请改成 import 'recorder-core'，或随便调用一下 Recorder.a=1 保证强引用

/** H5、小程序环境中：引入需要的格式编码器、可视化插件，App环境中在renderjs中引入 **/
// 注意：如果App中需要在逻辑层中调用Recorder的编码/转码功能，需要去掉此条件编译，否则会报未加载编码器的错误
// #ifdef H5 || MP-WEIXIN
  //按需引入需要的录音格式编码器，用不到的不需要引入，减少程序体积；H5、renderjs中可以把编码器放到static文件夹里面用动态创建script来引入，免得这些文件太大
  import 'recorder-core/src/engine/mp3.js'
  import 'recorder-core/src/engine/mp3-engine.js'
  import 'recorder-core/src/engine/wav.js'
  import 'recorder-core/src/engine/pcm.js'
  import 'recorder-core/src/engine/g711x'

  //可选引入可视化插件
  import 'recorder-core/src/extensions/waveview.js'
  import 'recorder-core/src/extensions/wavesurfer.view.js'

  import 'recorder-core/src/extensions/frequency.histogram.view.js'
  import 'recorder-core/src/extensions/lib.fft.js'

  //实时播放语音，仅支持h5
  import 'recorder-core/src/extensions/buffer_stream.player.js'
  //测试用根据简谱生成一段音乐
  import 'recorder-core/src/extensions/create-audio.nmn2pcm.js'
// #endif

/** 引入RecordApp **/
import RecordApp from 'recorder-core/src/app-support/app.js'
//【所有平台必须引入】uni-app支持文件
import '../../uni_modules/Recorder-UniCore/app-uni-support.js'

var disableOgg=false;
// #ifdef MP-WEIXIN
  //可选引入微信小程序支持文件
  import 'recorder-core/src/app-support/app-miniProgram-wx-support.js'
  disableOgg=true; //小程序不测试ogg js文件太大
// #endif


// #ifdef H5 || MP-WEIXIN
  //H5、renderjs中可以把编码器放到static文件夹里面用动态创建script来引入，免得这些文件太大
  import 'recorder-core/src/engine/beta-amr'
  import 'recorder-core/src/engine/beta-amr-engine'
// #endif
// #ifdef H5
  //app、h5测试ogg，小程序不测试ogg js文件太大
  import 'recorder-core/src/engine/beta-ogg'
  import 'recorder-core/src/engine/beta-ogg-engine'
// #endif


/** 可选：App中引入原生录音插件来进行录音，兼容性和体验更好，原生插件市场地址: https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin （试用无任何限制）
  在调用RecordApp.RequestPermission之前进行配置，建议放到import后面直接配置（全局生效）
  也可以判断一下只在iOS上或Android上启用，不判断就都启用，比如判断iOS：RecordApp.UniIsApp()==2 */
RecordApp.UniNativeUtsPlugin={nativePlugin:true}; //目前仅支持原生插件，uts插件不可用

//App中提升后台录音的稳定性：配置了原生插件后，可配置 `RecordApp.UniWithoutAppRenderjs=true` 禁用renderjs层音频编码（WebWorker加速），变成逻辑层中直接编码（但会降低逻辑层性能），后台运行时可避免部分手机WebView运行受限的影响

//App中提升后台录音的稳定性：需要启用后台录音保活服务（iOS不需要），Android 9开始，锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音（renderjs中H5录音也受影响），请调用配套原生插件的`androidNotifyService`接口，或使用第三方保活插件




export default {
  components: { TestPlayer,TestUploadView,TestRtVoiceView,TestNativePluginView,TestPageRenderjsView },
  data() {
    return {
      recType:"mp3"
      ,recSampleRate:16000
      ,recBitRate:16

      ,takeoffEncodeChunkSet:false
      ,takeoffEncodeChunkMsg:""
      ,useAEC:false
      ,useANotifySrv:true
      ,appUseH5Rec:false
      ,showUpload:false

      ,recwaveChoiceKey:"WaveView"
      ,recpowerx:0
      ,recpowert:""
      ,pageDeep:0,pageNewPath:"main_recTest"
      ,disableOgg:disableOgg
      ,evalExecCode:""
      ,testMsgs:[],reclogs:[],reclogLast:""
    }
  },
  mounted() {
    var vueVer=[];
    var vv=typeof(Vue)!="undefined" && Vue && Vue.version; if(vv) vueVer.push("Vue.version:"+vv);
    var v3=(((this.$||{}).appContext||{}).app||{}).version; if(v3) vueVer.push("appContext.app.version:"+v3);
    var v2=(((this.$root||{}).constructor||{}).super||{}).version; if(v2) vueVer.push("constructor.super:"+v2);
    this.reclog("页面mounted("+getCurrentPages().length+"层)"
      +"，Vue="+vueVer.join("/")
      +"，WebViewId="+(this.$root.$page&&this.$root.$page.id||"?")
      +"，ComponentId=_$id:"+(this._$id||"?")+"/"+"$.uid:"+(this.$&&this.$.uid||"?")
      +"，Recorder.LM="+Recorder.LM
      +"，RecordApp.LM="+RecordApp.LM
      +"，UniSupportLM="+RecordApp.UniSupportLM
      +"，UniJsSource="+RecordApp.UniJsSource.IsSource);
    this.pageDeep=getCurrentPages().length;
    this.pageNewPath=/main_recTest/.test(this.getRouteStr())?"page_index2":"main_recTest";

    this.isMounted=true; this.uniPage__onShow(); //onShow可能比mounted先执行，页面准备好了时再执行一次

    //可选，立即显示出环境信息
    this.reclog("正在执行Install，请勿操作...","#f60");
    RecordApp.Install(()=>{
      this.reclog("Install成功，环境："+this.currentKeyTag(),2);
      this.reclog("请先请求录音权限，然后再开始录音");
    },(err)=>{
      this.reclog("RecordApp.Install出错："+err,1);
    });
  },
  /*#ifdef VUE3*/unmounted()/*#endif*/ /*#ifndef VUE3*/destroyed()/*#endif*/ {
    RecordApp.Stop(); //清理资源，如果打开了录音没有关闭，这里将会进行关闭
  },
  onShow() { //当组件用时没这个回调
    if(this.isMounted) this.uniPage__onShow(); //onShow可能比mounted先执行，页面可能还未准备好
  },
  methods:{
    uniPage__onShow(){ //页面onShow时【必须调用】的函数，传入当前组件this
      RecordApp.UniPageOnShow(this);
    }
    ,currentKeyTag(){
      if(!RecordApp.Current) return "[?]";
      // #ifdef APP
      var tag2="Renderjs+H5";
      if(RecordApp.UniNativeUtsPlugin){
        tag2=RecordApp.UniNativeUtsPlugin.nativePlugin?"NativePlugin":"UtsPlugin";
      }
      return RecordApp.Current.Key+"("+tag2+")";
      // #endif
      return RecordApp.Current.Key;
    }

    ,recReq(){
      if(this.appUseH5Rec){//测试时指定使用h5录音
        RecordApp.UniNativeUtsPlugin=null;
      }else{
        RecordApp.UniNativeUtsPlugin={nativePlugin:true}; //恢复原生插件配置值
        RecordApp.UniCheckNativeUtsPluginConfig(); //可以检查一下原生插件配置是否有效
        RecordApp.UniNativeUtsPlugin_JsCall=(data)=>{ //可以绑定原生插件的jsCall回调
          if(data.action=="onLog"){ //显示原生插件日志信息
            this.reclog("[Native.onLog]["+data.tag+"]"+data.message, data.isError?1:"#bbb", {noLog:1});
          }
        };
      }

      /****【在App内使用app-uni-support.js的授权许可】编译到App平台时仅供测试用（App平台包括：Android App、iOS App），不可用于正式发布或商用，正式发布或商用需先联系作者获得授权许可（编译到其他平台时无此授权限制，比如：H5、小程序，均为免费授权）
      获得授权许可后，请解开下面这行注释，并且将**部分改成你的uniapp项目的appid，即可解除所有限制；使用配套的原生录音插件或uts插件时可不进行此配置
      ****/
      //RecordApp.UniAppUseLicense='我已获得UniAppID=*****的商用授权';

      //使用renderjs时提示一下iOS有弹框
      if(RecordApp.UniIsApp() && !RecordApp.UniNativeUtsPlugin){
        this.reclog("当前是在App的renderjs中使用H5进行录音，iOS上只支持14.3以上版本，且iOS上每次进入页面后第一次请求录音权限时、或长时间无操作再请求录音权限时WebView均会弹出录音权限对话框，不同旧iOS版本（低于iOS17）下H5录音可能存在的问题在App中同样会存在；使用配套的原生录音插件或uts插件时无以上问题和版本限制，Android也无以上问题","#f60");
      }

      if(this.useAEC){ //这个是Start中的audioTrackSet配置，在h5（H5、App+renderjs）中必须提前配置，因为h5中RequestPermission会直接打开录音
        RecordApp.RequestPermission_H5OpenSet={ audioTrackSet:{ noiseSuppression:true,echoCancellation:true,autoGainControl:true } };
      }

      this.reclog("正在请求录音权限...");
      RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
      RecordApp.RequestPermission(()=>{
        this.reclog(this.currentKeyTag()+" 已获得录音权限，可以开始录音了",2);
        if(this.reqOkCall)this.reqOkCall(); this.reqOkCall=null; //留别的组件内调用的回调
      },(msg,isUserNotAllow)=>{
        if(isUserNotAllow){//用户拒绝了录音权限
          //这里你应当编写代码进行引导用户给录音权限，不同平台分别进行编写
        }
        this.reclog(this.currentKeyTag()+" "
          +(isUserNotAllow?"isUserNotAllow,":"")+"请求录音权限失败："+msg,1);
      });
    }
    ,recStart(){
      this.$refs.player.setPlayBytes(null);
      this.takeoffEncodeChunkMsg="";var takeEcCount=0,takeEcSize=0;
      this.takeEcChunks=this.takeoffEncodeChunkSet?[]:null;
      this.watchDogTimer=0; this.wdtPauseT=0; var processTime=0;

      if(this.useANotifySrv) this.tryStart_androidNotifyService(); //Android App+原生插件环境下，如需后台或锁屏录音，就必须启用后台录音保活服务（iOS不需要），Android 9开始，锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音（renderjs中H5录音、原生插件录音均受影响），因此需要调用原生插件的`androidNotifyService`接口保活，或使用第三方保活插件

      this.reclog(this.currentKeyTag()+" 正在打开...");
      RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
      RecordApp.Start({
        type:this.recType
        ,sampleRate:this.recSampleRate
        ,bitRate:this.recBitRate
        ,audioTrackSet:!this.useAEC?null:{ //配置回声消除，H5、App、小程序均可用，但并不一定会生效；注意：H5、App+renderjs中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
          noiseSuppression:true,echoCancellation:true,autoGainControl:true
        }

        ,onProcess:(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd)=>{
          //全平台通用：可实时上传（发送）数据，配合Recorder.SampleData方法，将buffers中的新数据连续的转换成pcm上传，或使用mock方法将新数据连续的转码成其他格式上传，可以参考Recorder文档里面的：Demo片段列表 -> 实时转码并上传-通用版；基于本功能可以做到：实时转发数据、实时保存数据、实时语音识别（ASR）等

          //注意：App里面是在renderjs中进行实际的音频格式编码操作，此处的buffers数据是renderjs实时转发过来的，修改此处的buffers数据不会改变renderjs中buffers，所以不会改变生成的音频文件，可在onProcess_renderjs中进行修改操作就没有此问题了；如需清理buffers内存，此处和onProcess_renderjs中均需要进行清理，H5、小程序中无此限制
          //注意：如果你要用只支持在浏览器中使用的Recorder扩展插件，App里面请在renderjs中引入此扩展插件，然后在onProcess_renderjs中调用这个插件；H5可直接在这里进行调用，小程序不支持这类插件；如果调用插件的逻辑比较复杂，建议封装成js文件，这样逻辑层、renderjs中直接import，不需要重复编写

          this.recpowerx=powerLevel;
          this.recpowert=this.formatTime(duration,1)+" / "+powerLevel;
          processTime=Date.now();

          //H5、小程序等可视化图形绘制，直接运行在逻辑层；App里面需要在onProcess_renderjs中进行这些操作
          // #ifdef H5 || MP-WEIXIN
          var wave=this.waveStore&&this.waveStore[this.recwaveChoiceKey];
          if(wave){
            wave.input(buffers[buffers.length-1],powerLevel,sampleRate);
          }
          // #endif
          //实时语音通话对讲，实时处理录音数据
          if(this.wsVoiceProcess) this.wsVoiceProcess(buffers,powerLevel,duration,sampleRate,newBufferIdx);

          //实时释放清理内存，用于支持长时间录音；在指定了有效的type时，编码器内部可能还会有其他缓冲，必须同时提供takeoffEncodeChunk才能清理内存，否则type需要提供unknown格式来阻止编码器内部缓冲，App的onProcess_renderjs中需要进行相同操作
          if(this.takeEcChunks){
            if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
            for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
            this.clearBufferIdx=newBufferIdx;
          }
        }
        ,onProcess_renderjs:`function(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd){
          //App中在这里修改buffers才会改变生成的音频文件
          //App中是在renderjs中进行的可视化图形绘制，因此需要写在这里，this是renderjs模块的this（也可以用This变量）；如果代码比较复杂，请直接在renderjs的methods里面放个方法xxxFunc，这里直接使用this.xxxFunc(args)进行调用
          var wave=this.waveStore&&this.waveStore[this.recwaveChoiceKey];
          if(wave) wave.input(buffers[buffers.length-1],powerLevel,sampleRate);

          //和onProcess中一样进行释放清理内存，用于支持长时间录音
          if(${this.takeEcChunks?1:0}){
            if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
            for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
            this.clearBufferIdx=newBufferIdx;
          }
        }`

        ,takeoffEncodeChunk:!this.takeoffEncodeChunkSet?null:(chunkBytes)=>{
          //全平台通用：实时接收到编码器编码出来的音频片段数据，chunkBytes是Uint8Array二进制数据，可以实时上传（发送）出去
          //App中如果未配置RecordApp.UniWithoutAppRenderjs时，建议提供此回调，因为录音结束后会将整个录音文件从renderjs传回逻辑层，由于uni-app的逻辑层和renderjs层数据交互性能实在太拉跨了，大点的文件传输会比较慢，提供此回调后可避免Stop时产生超大数据回传
          takeEcCount++; takeEcSize+=chunkBytes.byteLength;
          this.takeoffEncodeChunkMsg="已接收到"+takeEcCount+"块，共"+takeEcSize+"字节";
          this.takeEcChunks.push(chunkBytes);

          //App中使用原生插件时，可方便的将数据实时保存到同一文件，第一帧时append:false新建文件，后面的append:true追加到文件
          //RecordApp.UniNativeUtsPluginCallAsync("writeFile",{path:"xxx.mp3",append:回调次数!=1, dataBase64:RecordApp.UniBtoa(chunkBytes.buffer)}).then(...).catch(...)
        }
        ,takeoffEncodeChunk_renderjs:!this.takeoffEncodeChunkSet?null:`function(chunkBytes){
          //App中这里可以做一些仅在renderjs中才生效的事情，不提供也行，this是renderjs模块的this（也可以用This变量）
        }`

        ,start_renderjs:`function(){
          //App中可以放一个函数，在Start成功时renderjs中会先调用这里的代码，this是renderjs模块的this（也可以用This变量）
          //放一些仅在renderjs中才生效的事情，比如初始化，不提供也行
        }`
        ,stop_renderjs:`function(aBuf,duration,mime){
          //App中可以放一个函数，在Stop成功时renderjs中会先调用这里的代码，this是renderjs模块的this（也可以用This变量）
          this.audioData=aBuf; //留着给Stop时进行转码成wav播放
        }`
      },()=>{
        this.reclog(this.currentKeyTag()+" 录制中："+this.recType
          +" "+this.recSampleRate+" "+this.recBitRate+"kbps"
          +(this.takeoffEncodeChunkSet?" takeoffEncodeChunk":"")
          +(this.useAEC?" useAEC":"")
          +(this.appUseH5Rec?" appUseH5Rec":""),2);
        //创建音频可视化图形绘制
        this.initWaveStore();

        //【稳如老狗WDT】可选的，监控是否在正常录音有onProcess回调，如果长时间没有回调就代表录音不正常
        if(RecordApp.Current.CanProcess()){
          var wdt=this.watchDogTimer=setInterval(()=>{
            if(wdt!=this.watchDogTimer){ clearInterval(wdt); return } //sync
            if(Date.now()<this.wdtPauseT) return; //如果暂停录音了就不检测：puase时赋值this.wdtPauseT=Date.now()*2（永不监控），resume时赋值this.wdtPauseT=Date.now()+1000（1秒后再监控）
            if(Date.now()-(processTime||startTime)>1500){ clearInterval(wdt);
              this.reclog(processTime?"录音被中断":"录音未能正常开始",1);
              // ... 错误处理，关闭录音，提醒用户
            }
          },1000);
        }else{
          this.reclog("当前环境不支持onProcess回调，不启用watchDogTimer","#aaa"); //目前都支持回调
        }
        var startTime=Date.now();
      },(msg)=>{
        this.reclog(this.currentKeyTag()+" 开始录音失败："+msg,1);
      });
    }
    ,recPause(){
      if(RecordApp.GetCurrentRecOrNull()){
        RecordApp.Pause();
        this.wdtPauseT=Date.now()*2; //永不监控onProcess超时
        this.reclog("已暂停");
      }
    }
    ,recResume(){
      if(RecordApp.GetCurrentRecOrNull()){
        RecordApp.Resume();
        this.wdtPauseT=Date.now()+1000; //1秒后再监控onProcess超时
        this.reclog("继续录音中...");
      }
    }
    ,recStopX(){
      this.tryClose_androidNotifyService(); //关闭后台录音保活服务
      this.watchDogTimer=0; //停止监控onProcess超时
      RecordApp.Stop(
        null //success传null就只会清理资源，不会进行转码
        ,(msg)=>{
          this.reclog("已清理，错误信息："+msg);
        }
      );
    }
    ,recStop(){
      this.reclog("正在结束录音...");
      this.tryClose_androidNotifyService(); //关闭后台录音保活服务
      this.watchDogTimer=0; //停止监控onProcess超时

      RecordApp.Stop((aBuf,duration,mime)=>{
        //全平台通用：aBuf是ArrayBuffer音频文件二进制数据，可以保存成文件或者发送给服务器
        //App中如果在Start参数中提供了stop_renderjs，renderjs中的函数会比这个函数先执行

        var recSet=(RecordApp.GetCurrentRecOrNull()||{set:{type:this.recType}}).set;
        var testPlay_aBuf_renderjs="this.audioData";
        this.reclog("已录制["+mime+"]："+this.formatTime(duration,1)+" "+aBuf.byteLength+"字节 "
            +recSet.sampleRate+"hz "+recSet.bitRate+"kbps",2);

        //如果使用了takeoffEncodeChunk，Stop的aBuf长度是0，数据早已存到了takeEcChunks数组里面，直接合并成完整音频文件
        if(this.takeEcChunks){
          testPlay_aBuf_renderjs=""; //renderjs的数据是空的
          this.reclog("启用takeoffEncodeChunk后Stop返回的blob长度为0不提供音频数据");
          var len=0; for(var i=0;i<this.takeEcChunks.length;i++)len+=this.takeEcChunks[i].length;
          var chunkData=new Uint8Array(len);
          for(var i=0,idx=0;i<this.takeEcChunks.length;i++){
            var itm=this.takeEcChunks[i]; chunkData.set(itm,idx); idx+=itm.length;
          }
          aBuf=chunkData.buffer;
          this.reclog("takeoffEncodeChunk接收到的音频片段，已合并成一个音频文件 "+aBuf.byteLength+"字节");
        }


        /**【保存文件】【上传】示例，详细请参考 test_upload_saveFile.vue 文件
        //如果是H5环境，也可以直接构造成Blob/File文件对象，和Recorder使用一致
        // #ifdef H5
          var blob=new Blob([arrayBuffer],{type:mime});
          console.log(blob, (window.URL||webkitURL).createObjectURL(blob));
          var file=new File([arrayBuffer],"recorder.mp3");
          //uni.uploadFile({file:file, ...}) //直接上传
        // #endif

        //如果是App、小程序环境，可以直接保存到本地文件，然后调用相关网络接口上传
        // #ifdef APP || MP-WEIXIN
          RecordApp.UniSaveLocalFile("recorder.mp3",arrayBuffer,(savePath)=>{
            console.log(savePath); //app保存的文件夹为`plus.io.PUBLIC_DOWNLOADS`，小程序为 `wx.env.USER_DATA_PATH` 路径
            //uni.uploadFile({filePath:savePath, ...}) //直接上传
          },(errMsg)=>{ console.error(errMsg) });
        // #endif
        **/


        //【测试】用变量保存起来，别的地方调用
        this.lastRecType=recSet.type;
        this.lastRecBuffer=aBuf;

        //【测试】播放，部分格式会转码成wav播放
        this.$refs.player.setPlayBytes(aBuf,testPlay_aBuf_renderjs,duration,mime,recSet,Recorder);
      },(msg)=>{
        this.reclog("结束录音失败："+msg,1);
      });
    }


    //Android App启用后台录音保活服务，需要原生插件支持
    ,tryStart_androidNotifyService(){
      if(RecordApp.UniIsApp() && !this._tips_anfs){ this._tips_anfs=1;
        this.reclog("App中提升后台录音的稳定性：需要启用后台录音保活服务（iOS不需要），Android 9开始，锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音（App中H5录音也受影响），需要原生层提供搭配常驻通知的Android后台录音保活服务（Foreground services）；可调用配套原生插件的androidNotifyService接口，或使用第三方保活插件","#4face6");
      }
      if(RecordApp.UniIsApp()!=1) return; //非Android App不处理
      if(!RecordApp.UniNativeUtsPlugin) return; //未使用原生插件

      this._NotifyService=false;
      RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{
        title:"正在录音"
        ,content:"正在录音中，请勿关闭App运行"
      }).then((data)=>{
        this._NotifyService=true;
        var nCode=data.notifyPermissionCode, nMsg=data.notifyPermissionMsg;
        this.reclog("搭配常驻通知的Android后台录音保活服务已打开，ForegroundService已运行(通知可能不显示或会延迟显示，并不影响服务运行)，通知显示状态(1有通知权限 3可能无权限)code="+nCode+" msg="+nMsg,2);
      }).catch((e)=>{
        this.reclog("原生插件的androidNotifyService接口调用出错："+e.message,1);
        this.reclog("如果你已集成了配套的原生录音插件，并且是打包自定义基座运行，请检查本项目根目录的AndroidManifest.xml里面是否已经解开了注释，否则被注释掉的service不会包含在App中",1);
      });
    }
    ,tryClose_androidNotifyService(){
      if(!this._NotifyService) return;   this._NotifyService=false;
      RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{
        close:true
      }).then(()=>{
        this.reclog("已关闭搭配常驻通知的Android后台录音保活服务");
      }).catch((e)=>{
        this.reclog("原生插件的androidNotifyService接口调用出错："+e.message,1);
      });
    }


    ,clearLogs(){ this.testMsgs=[]; this.reclogs=[]; }
    ,addTestMsg(msg,color){
      var now=new Date();
      var t=("0"+now.getHours()).substr(-2)
        +":"+("0"+now.getMinutes()).substr(-2)
        +":"+("0"+now.getSeconds()).substr(-2);
      var txt="["+t+"]"+msg;
      this.testMsgs.splice(0,0,{msg:txt,color:color});
    }
    ,reclog(msg,color,set){
      var now=new Date();
      var t=("0"+now.getHours()).substr(-2)
        +":"+("0"+now.getMinutes()).substr(-2)
        +":"+("0"+now.getSeconds()).substr(-2);
      var txt="["+t+"]"+msg;
      if(!set||!set.noLog)console.log(txt);
      this.reclogs.splice(0,0,{txt:txt,color:color});
      this.reclogLast={txt:txt,color:color};
    }
    ,recTypeClick(e){
      var type=e.target.dataset.type;
      if(type){
        this.recType=type;
      }
    }
    ,appUseH5RecClick(){
      this.appUseH5Rec=!this.appUseH5Rec;
      RecordApp.Current=null;
      this.reclog('切换了appUseH5Rec='+this.appUseH5Rec+'，重新请求录音权限后生效',"#f60");
    }

    // 可视化波形，这里是一次性创建多个波形，可以参考page_i18n.vue只创建一个波形会简单一点
    ,initWaveStore(){
      //if(isWx && this.waveStore)return;//可以限制初始化一次；如果你的canvas所在的view存在v-if之类的会重新创建了对应的view，必须将波形重新进行初始化才能使用；如果使用的是同一个view，重新初始化后如果上次的动画没有完成时，小程序中开头部分新的波形会和老的波形相互干扰，老动画完成后恢复正常，App、H5无此问题
      var store=this.waveStore=this.waveStore||{};//这个测试demo会创建多个可视化，所以用个对象存起来，只有一个的时候请直接用个变量存一下即可 不用搞这么复杂
      var webStore=`var store=this.waveStore=this.waveStore||{};`;//在renderjs中执行，this是renderjs模块的this
      webStore+=`this.recwaveChoiceKey="${this.recwaveChoiceKey}";`;//把当前选中的波形也传过去

      //App环境下是在renderjs中绘制，H5、小程序等是在逻辑层中绘制，因此需要提供两段相同的代码（宽高值需要和canvas style的宽高一致）
      RecordApp.UniFindCanvas(this,[".recwave-WaveView"],`${webStore}
        store.WaveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
      `,(canvas1)=>{
        store.WaveView=Recorder.WaveView({compatibleCanvas:canvas1, width:300, height:100});
      });

      RecordApp.UniFindCanvas(this,[".recwave-SurferView",".recwave-SurferView-2x"],`${webStore}
        store.SurferView=Recorder.WaveSurferView({compatibleCanvas:canvas1,compatibleCanvas_2x:canvas2, width:300, height:100});
      `,(canvas1,canvas2)=>{
        store.SurferView=Recorder.WaveSurferView({compatibleCanvas:canvas1,compatibleCanvas_2x:canvas2, width:300, height:100});
        //注意：iOS上微信小程序基础库存在bug，canvas.drawImage(canvas)可能无法绘制，可能会导致WaveSurferView在iOS小程序上不能正确显示，其他环境下无此兼容性问题
      });

      RecordApp.UniFindCanvas(this,[".recwave-Histogram1"],`${webStore}
        store.Histogram1=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:300, height:100});
      `,(canvas1)=>{
        store.Histogram1=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:300, height:100});
      });
      RecordApp.UniFindCanvas(this,[".recwave-Histogram2"],`${webStore}
        store.Histogram2=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:300, height:100
          ,lineCount:200,widthRatio:1,position:0,minHeight:1
          ,fallDuration:600,stripeEnable:false,mirrorEnable:true});
      `,(canvas1)=>{
        store.Histogram2=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:300, height:100
          ,lineCount:200,widthRatio:1,position:0,minHeight:1
          ,fallDuration:600,stripeEnable:false,mirrorEnable:true});
      });
      RecordApp.UniFindCanvas(this,[".recwave-Histogram3"],`${webStore}
        store.Histogram3=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:300, height:100
          ,lineCount:20,position:0,minHeight:1,fallDuration:400,stripeEnable:false,mirrorEnable:true
          ,linear:[0,"#0ac",1,"#0ac"]});
      `,(canvas1)=>{
        store.Histogram3=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:300, height:100
          ,lineCount:20,position:0,minHeight:1,fallDuration:400,stripeEnable:false,mirrorEnable:true
          ,linear:[0,"#0ac",1,"#0ac"]});
      });
    }
    ,recwaveChoice(e){
      var key=e.target.dataset.key;
      if(key){
        if(key!=this.recwaveChoiceKey){
          this.reclog("已切换波形显示为："+key);
          if(key=="SurferView"){
            // #ifdef MP-WEIXIN
            this.reclog("注意：iOS上微信小程序基础库存在bug，canvas.drawImage(canvas)可能无法绘制，可能会导致WaveSurferView在iOS小程序上不能正确显示，其它可视化插件无此兼容性问题","#fa0");
            // #endif
          }
        }
        this.recwaveChoiceKey=key;
        //App中传送给renderjs里面，同样赋值
        if(RecordApp.UniIsApp()){
          RecordApp.UniWebViewVueCall(this,'this.recwaveChoiceKey="'+key+'"');
        }
      }
    }

    //注入60秒数据，方便测试
    ,recEnvIn60(){
      var rec=RecordApp.GetCurrentRecOrNull();
      if(!rec){
        this.reclog("未开始录音，无法注入",1);
        return;
      }
      if(RecordApp.UniIsApp()){
        //App中到renderjs里面里面注入
        RecordApp.UniWebViewVueCall(this,`
          var rec=RecordApp.GetCurrentRecOrNull();
          var sampleRate=rec.srcSampleRate,t1=Date.now();
          var canon=Recorder.NMN2PCM.GetExamples().Canon.get(sampleRate).pcm;
          var len=sampleRate*60,offset=0;
          while(offset<len){
            rec.envIn(canon.subarray(0,Math.min(canon.length,len-offset)),0);
            offset+=canon.length;
          }
          this.$ownerInstance.callMethod("reclog","已注入60秒Canon简谱生成的音乐，耗时"+(Date.now()-t1)+"ms");
        `);
        return;
      }
      var sampleRate=rec.srcSampleRate,t1=Date.now();
      var canon=Recorder.NMN2PCM.GetExamples().Canon.get(sampleRate).pcm;
      var len=sampleRate*60,offset=0;
      while(offset<len){
        rec.envIn(canon.subarray(0,Math.min(canon.length,len-offset)),0);
        offset+=canon.length;
      }
      this.reclog("已注入60秒Canon简谱生成的音乐，耗时"+(Date.now()-t1)+"ms");
    }

    ,formatTime(ms,showSS){
      var ss=ms%1000;ms=(ms-ss)/1000;
      var s=ms%60;ms=(ms-s)/60;
      var m=ms%60;ms=(ms-m)/60;
      var h=ms, v="";
      if(h>0) v+=(h<10?"0":"")+h+":";
      v+=(m<10?"0":"")+m+":";
      v+=(s<10?"0":"")+s;
      if(showSS)v+="″"+("00"+ss).substr(-3);;
      return v;
    }
    ,getRouteStr(){
      var url=this.$page&&this.$page.route||this.$root.route; //vue2 || vue3
      if(!url && this.$root.$scope){//wx
        url=this.$root.$scope.route;
      }
      return "/"+url;
    }
    ,reloadPage(){
      var url=this.getRouteStr();
      console.log("刷新页面 url="+url);
      if(getCurrentPages().length==1){
        uni.reLaunch({ url:url })
      }else{
        uni.navigateBack({animationDuration:0,success:()=>{ setTimeout(()=>{
          uni.navigateTo({url:url})
        },300); }});
      }
    }
    ,evalExecClick(){
      if(!this.evalExecCode){
        this.reclog("请填写要执行的代码",1);
        return;
      }
      // #ifdef APP || H5
      try{
        new Function("Recorder,RecordApp",this.evalExecCode).call(this,Recorder,RecordApp);
        this.reclog("代码已执行",2);
      }catch(e){
        this.reclog("代码执行异常："+e.message,1);
      }
      // #endif
    }

    //测试renderjs功能调用
    ,testRenderjsFunc(){
      this.$refs.testRF.showTest();
    }
    //测试原生插件功能
    ,testNativePlugin(){
      this.$refs.testNP.showTest();
    }
    //显示内存占用
    ,testShowMemoryUsage(){
      this.$refs.testNP.showMemoryUsage();
    }
    //显示Android后台录音保活通知服务
    ,testShowNotifyService(){
      this.$refs.testNP.showNotifyService(true);
    }
    ,testCloseNotifyService(){
      this.$refs.testNP.showNotifyService(false);
    }
    //切换扬声器外放和听筒播放
    ,speakerOnClick(){
      this.__setSpeakerOff(false);
    }
    ,speakerOffClick(){
      this.__setSpeakerOff(true);
    }
    ,__setSpeakerOff(off){
      // #ifdef APP
      RecordApp.UniNativeUtsPluginCallAsync("setSpeakerOff",{off:off,headset:off}).then(()=>{
        this.reclog("已切换成"+(off?"听筒播放":"外放"),2);
      }).catch((e)=>{
        this.reclog("切换"+(off?"听筒播放":"外放")+"失败："+e.message,1);
      });
      return;
      // #endif
      // #ifdef MP-WEIXIN
      wx.setInnerAudioOption({ speakerOn:!off, success:()=>{
        this.reclog("已切换成"+(off?"听筒播放":"外放"),2);
      }, fail:(e)=>{
        this.reclog("切换"+(off?"听筒播放":"外放")+"失败："+e.errMsg,1);
      }});
      return;
      // #endif
      return this.reclog("目前仅App或小程序支持支持切换外放和听筒播放",1);
    }

    //H5播放5分钟wav
    ,testH5Play5F(){
      var jsCode=`(function(log){
        var scope=window.testH5Play5FScope=window.testH5Play5FScope||{};
        var el=document.querySelector(".testH5Play5FView");
        if(!scope.init){ scope.init=1;
          el.innerHTML='<div style="padding-top:8px"><audio class="testH5Play5FAudio" controls style="width:100%"></audio></div>';
        }
        var audio=document.querySelector(".testH5Play5FAudio");
        audio.onerror=function(e){ log('播放失败['+audio.error.code+']'+audio.error.message,1) };
        audio.onpause=function(e){ log('已停止播放') };
        audio.onplay=function(e){ log("正在播放5分钟wav",2) };
        var urlOk=function(){
          if(!(audio.ended || audio.paused)){ audio.pause(); return }
          audio.src=scope.url; audio.play();
        }; if(scope.url){ urlOk(); }else{
          log("正在合成5分钟音频..."); setTimeout(function(){ var sr=16000;
          var pcm=Recorder.NMN2PCM.GetExamples().Canon.get(sr).pcm;
          var pcm5=new Int16Array(5*60*sr),n=0;
          while(n<pcm5.length){
            var s=Math.min(pcm.length, pcm5.length-n);
            pcm5.set(s==pcm.length?pcm:pcm.subarray(0,s),n); n+=pcm.length
          } log("正在转码5分钟wav...");
          var mock=Recorder({ type:"wav",sampleRate:sr,bitRate:16 }); mock.mock(pcm5, sr);
          mock.stop(function(blob){
            scope.url=(window.URL||webkitURL).createObjectURL(blob); urlOk();
          },function(err){ log(err,1); });
        }); }
      })`;
      var logFn=(msg,color)=>{ this.addTestMsg("[Play5F]"+msg,color) };
      /*#ifdef H5*/ eval(jsCode)(logFn); return; /*#endif*/
      /*#ifdef APP*/
      var cb=RecordApp.UniMainCallBack_Register("Play5F",(data)=>{ logFn(data.msg,data.color) });
      RecordApp.UniWebViewEval(this,jsCode+`(function(msg,color){
        RecordApp.UniWebViewSendToMain({action:"${cb}",msg:msg,color:color});
      })`);
      return;
      /*#endif*/
      this.reclog("当前环境未适配播放",1)
    }

    ,loadVConsole(){
      var isApp=false, isH5=false;
      /*#ifdef APP*/ isApp=true; /*#endif*/
      /*#ifdef H5*/ isH5=true; /*#endif*/
      var jsCode=`(function(){
        var isApp=${isApp}, isH5=${isH5};
        var ok=function(){
          if(isApp){
            This.$ownerInstance.callMethod("reclog","vConsole已加载");
          }else{
            This.reclog("vConsole已加载");
          }
        }
        if(window.VConsole)return ok();
        var elem=document.createElement("script");
        elem.setAttribute("type","text/javascript");
        elem.setAttribute("src","https://xiangyuecn.github.io/Recorder/assets/ztest-vconsole.js");
        document.body.appendChild(elem);
        elem.onload=function(){
          new VConsole(); ok()
        };
      })()`;
      this.reclog("正在renderjs中加载vConsole...");
      if(isApp){
        RecordApp.UniWebViewVueCall(this,jsCode);
      }else if(isH5){// #ifdef H5
        eval("var This=this;"+jsCode); // #endif
      }else{
        this.reclog("非app环境，不加载vConsole",1)
      }
    }
  }
}
</script>

<!-- #ifdef APP -->
<script module="testMainVue" lang="renderjs">
//此模块内部只能用选项式API风格，vue2、vue3均可用；不可改成setup组合式API风格，否则可能不能import vue导致编译失败
 /**============= App中在renderjs中引入RecordApp，这样App中也能使用H5录音、音频可视化 =============**/
 /** 先引入Recorder **/
 import Recorder from 'recorder-core'; //注意如果未引用Recorder变量，可能编译时会被优化删除（如vue3 tree-shaking），请改成 import 'recorder-core'，或随便调用一下 Recorder.a=1 保证强引用

 //按需引入需要的录音格式编码器，用不到的不需要引入，减少程序体积；H5、renderjs中可以把编码器放到static文件夹里面用动态创建script来引入，免得这些文件太大
 import 'recorder-core/src/engine/mp3.js'
 import 'recorder-core/src/engine/mp3-engine.js'
 import 'recorder-core/src/engine/wav.js'
 import 'recorder-core/src/engine/pcm.js'
 import 'recorder-core/src/engine/g711x'
 import 'recorder-core/src/engine/beta-amr'
 import 'recorder-core/src/engine/beta-amr-engine'
 import 'recorder-core/src/engine/beta-ogg'
 import 'recorder-core/src/engine/beta-ogg-engine'

 //可选引入可视化插件
 import 'recorder-core/src/extensions/waveview.js'
 import 'recorder-core/src/extensions/wavesurfer.view.js'

 import 'recorder-core/src/extensions/frequency.histogram.view.js'
 import 'recorder-core/src/extensions/lib.fft.js'

 //实时播放语音，仅支持h5
 import 'recorder-core/src/extensions/buffer_stream.player.js'
 //测试用根据简谱生成一段音乐
 import 'recorder-core/src/extensions/create-audio.nmn2pcm.js'

 /** 引入RecordApp **/
 import RecordApp from 'recorder-core/src/app-support/app.js'
 //【必须引入】uni-app支持文件
 import '../../uni_modules/Recorder-UniCore/app-uni-support.js'

 export default {
   mounted(){
     //App的renderjs必须调用的函数，传入当前模块this
     RecordApp.UniRenderjsRegister(this);

     this.$ownerInstance.callMethod("reclog","renderjs mounted"
       +"，WebViewId="+window.__WebVieW_Id__
       +"，ComponentId=_$id:"+(this._$id||"?")+"/$vm.ownerId:"+(this.$ownerInstance.$vm.ownerId||"?"));
   },
   methods: {
     //这里定义的方法，在逻辑层中可通过 RecordApp.UniWebViewVueCall(this,'this.xxxFunc()') 直接调用
     //调用逻辑层的方法，请直接用 this.$ownerInstance.callMethod("xxxFunc",{args}) 调用，二进制数据需转成base64来传递
   }
 }
</script>
<!-- #endif -->

<style>
.recwaveChoice {
  cursor: pointer;
  display: inline-block;
  vertical-align: bottom;
  border-right: 1px solid #ccc;
  background: #ddd;
  line-height: 28px;
  font-size: 12px;
  color: #666;
  padding: 0 5px;
}
.recwaveChoice:first-child {
  border-radius: 99px 0 0 99px;
}
.recwaveChoice:last-child {
  border-radius: 0 99px 99px 0;
  border-right: none;
}
.recwaveChoice.slc,
.recwaveChoice:hover {
  background: #f60;
  color: #fff;
}

.recwave {
  border: 1px solid #ccc;
  height: 100px;
  width: 300px;
}
.recwave canvas {
  height: 100px;
  width: 300px;
}
</style>
